import {
  Injectable,
  UnauthorizedException,
  ConflictException,
  Logger,
  NotFoundException,
  ForbiddenException,
  InternalServerErrorException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { ModuleRef } from '@nestjs/core';
import * as argon2 from 'argon2';
import { UsersService } from '../users/users.service';
import { DevicesService } from '../devices/devices.service';
import { RegisterDto, LoginDto, AdminLoginDto, RenewTokenDto, LogoutDto } from './dto';
import { RedisService } from './redis/redis.service';
import { UserRole, DeviceStatus, AuditStatus } from '../common/enums';
import {
  AdminTokenRenewalResponseDto,
  UserTokenRenewalResponseDto,
  AdminAuthTokenResponseDto,
  UserAuthTokenResponseDto,
} from './dto/auth-response.dto';
import { JwtTokenPayload } from './interfaces/jwt-token-payload.interface';
import { DeviceAuditService } from '../device-audit/device-audit.service';

@Injectable()
export class AuthService {
  private readonly logger = new Logger(AuthService.name);

  constructor(
    private readonly usersService: UsersService,
    private readonly devicesService: DevicesService,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    private readonly redisService: RedisService,
    private readonly moduleRef: ModuleRef,
  ) {}

  /**
   * Register a new user
   * @param registerDto Registration data
   * @returns Created user
   */
  async register(registerDto: RegisterDto) {
    const { username, password, deviceId, deviceSerialNumber, displayName } = registerDto;

    // Use deviceSerialNumber if provided, otherwise fall back to deviceId
    const serialNumber = deviceSerialNumber || deviceId;

    if (!serialNumber) {
      throw new UnauthorizedException('Device serial number is required');
    }

    // Check if username already exists
    const existingUser = await this.usersService.findByUsername(username);
    if (existingUser) {
      throw new ConflictException('Username already exists');
    }

    // Check if device exists and is not assigned to another user
    const device = await this.devicesService.findBySerialNumber(serialNumber);
    if (!device) {
      throw new NotFoundException('Device not found');
    }

    if (device.userId) {
      throw new ConflictException('Device already assigned to another user');
    }

    // Hash the password
    const hashedPassword = await this.hashPassword(password);

    // Create the user
    const user = await this.usersService.create({
      username,
      passwordHash: hashedPassword,
      role: UserRole.USER,
      displayName,
    });

    // Assign device to user
    await this.devicesService.update(device.id, {
      userId: user.id,
      status: DeviceStatus.ACTIVE,
    });

    this.logger.log(`User ${username} registered with device ${serialNumber}`);

    return {
      id: user.id,
      username: user.username,
      role: user.role,
      deviceSerialNumber: serialNumber,
      deviceId: serialNumber, // Include deviceId for backward compatibility
    };
  }

  /**
   * Admin login
   * @param adminLoginDto Admin login data
   * @returns JWT token and refresh token
   */
  async adminLogin(adminLoginDto: AdminLoginDto): Promise<AdminAuthTokenResponseDto> {
    const { username, password } = adminLoginDto;

    // Find user by username
    const user = await this.usersService.findByUsername(username);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    // Verify password
    const isPasswordValid = await this.verifyPassword(password, user.passwordHash);
    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    // Check if user is an admin
    if (user.role !== UserRole.ADMIN) {
      throw new UnauthorizedException('Not an admin account');
    }

    // Generate refresh token
    const refreshToken = this.redisService.generateRefreshToken();

    // Create session in Redis for admin
    const sessionId = await this.redisService.createSession(
      user.id,
      null, // No device ID for admin
      refreshToken, // Add refresh token for admin
    );

    // Generate JWT token
    const payload: JwtTokenPayload = {
      sub: user.id,
      username: user.username,
      role: user.role,
      sessionId,
    };

    const accessToken = this.jwtService.sign(payload, {
      secret: this.configService.get('JWT_SECRET'),
      expiresIn: '1h', // Set to 1 hour
    });

    this.logger.log(`Admin ${username} logged in`);

    // Return admin response without status field
    return {
      accessToken: accessToken,
      refreshToken: refreshToken,
      expiresIn: 3600, // 1 hour in seconds
      userId: user.id,
    };
  }

  /**
   * User login
   * @param loginDto User login data
   * @returns JWT token and refresh token
   */
  async login(loginDto: LoginDto): Promise<UserAuthTokenResponseDto> {
    const { username, password, deviceId, deviceSerialNumber } = loginDto;

    // Use deviceSerialNumber if provided, otherwise fall back to deviceId
    const serialNumber = deviceSerialNumber || deviceId;

    if (!serialNumber) {
      throw new UnauthorizedException('Device serial number is required');
    }

    // Step 1: Authenticate - Check username and password first
    // Find user by username
    const user = await this.usersService.findByUsername(username);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    // Verify password
    const isPasswordValid = await this.verifyPassword(password, user.passwordHash);
    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    // Step 2: Authorize - Check if device exists and is assigned to this user
    // Check if device exists
    const device = await this.devicesService.findBySerialNumber(serialNumber);
    if (!device) {
      this.logger.warn(
        `Login attempt with non-existent device: ${serialNumber} for user: ${username}`,
      );
      throw new ForbiddenException('Device not found');
    }

    // Check if device is assigned to this user
    if (device.userId !== user.id) {
      this.logger.warn(
        `Login attempt with device not assigned to user: ${username}, deviceSerialNumber: ${serialNumber}`,
      );
      throw new ForbiddenException('Device not assigned to this user');
    }

    // Check if device is active
    if (device.status !== DeviceStatus.ACTIVE) {
      this.logger.warn(`Login attempt with inactive device: ${serialNumber} for user: ${username}`);
      throw new ForbiddenException('Device is not active');
    }

    // Generate refresh token
    const refreshToken = this.redisService.generateRefreshToken();

    // Create session in Redis
    const sessionId = await this.redisService.createSession(user.id, serialNumber, refreshToken);

    // Generate JWT token
    const payload: JwtTokenPayload = {
      sub: user.id,
      username: user.username,
      role: user.role,
      deviceSerialNumber: serialNumber,
      sessionId,
    };

    const accessToken = this.jwtService.sign(payload, {
      secret: this.configService.get('JWT_SECRET'),
      expiresIn: '1h', // Set to 1 hour
    });

    this.logger.log(`User ${username} logged in with device ${serialNumber}`);

    // Get the audit status for this user+device combination
    // Since we've already checked that the device is active and belongs to the user,
    // this should always return PASS, but using the method for consistency
    const auditStatus = await this.getUserAuditStatusBySerialNumber(user.id, serialNumber);

    return {
      accessToken: accessToken,
      refreshToken: refreshToken,
      expiresIn: 3600, // 1 hour in seconds
      userId: user.id,
      status: auditStatus,
    };
  }

  /**
   * Renew access token using refresh token
   * @param renewTokenDto Renew token data
   * @returns New JWT token
   */
  async renewToken(
    renewTokenDto: RenewTokenDto,
  ): Promise<AdminTokenRenewalResponseDto | UserTokenRenewalResponseDto> {
    const { refreshToken } = renewTokenDto;

    try {
      // Find session by refresh token
      const sessionData = await this.redisService.findSessionByRefreshToken(refreshToken);
      if (!sessionData) {
        throw new UnauthorizedException('Invalid refresh token');
      }

      const { sessionId, session } = sessionData;

      // Get user
      const user = await this.usersService.findById(session.userId);
      if (!user) {
        throw new UnauthorizedException('User not found1');
      }

      // Generate new JWT token
      const newPayload: JwtTokenPayload = {
        sub: user.id,
        username: user.username,
        role: user.role,
        deviceSerialNumber: session.deviceSerialNumber,
        sessionId: sessionId,
      };

      const newAccessToken = this.jwtService.sign(newPayload, {
        secret: this.configService.get('JWT_SECRET'),
        expiresIn: '1h', // Set to 1 hour
      });

      this.logger.log(`Token renewed for user ${user.username}`);

      // Return different response based on user role
      if (user.role === UserRole.ADMIN) {
        // Admin token renewal response doesn't include status
        return {
          accessToken: newAccessToken,
          expiresIn: 3600, // 1 hour in seconds
        };
      } else {
        // For regular users, include their audit status in the response
        const auditStatus = await this.getUserAuditStatusBySerialNumber(
          user.id,
          session.deviceSerialNumber,
        );

        return {
          accessToken: newAccessToken,
          expiresIn: 3600, // 1 hour in seconds
          status: auditStatus,
        };
      }
    } catch (error) {
      if (error instanceof UnauthorizedException) {
        throw error;
      }
      this.logger.error(`Token renewal failed: ${error.message}`);
      throw new UnauthorizedException('Invalid token');
    }
  }

  /**
   * Logout
   * @param logoutDto Logout data
   * @returns Success message
   */
  async logout(logoutDto: LogoutDto) {
    const { sessionId } = logoutDto;

    const deleted = await this.redisService.deleteSession(sessionId);
    if (!deleted) {
      throw new NotFoundException('Session not found');
    }

    return { message: 'Logged out successfully' };
  }

  /**
   * Request password reset
   * @param email User email
   * @returns Success message
   */
  async requestPasswordReset(email: string) {
    // Implementation will depend on your email service
    this.logger.log(`Password reset requested for email: ${email}`);
    return { message: 'Password reset email sent' };
  }

  /**
   * Confirm password reset
   * @param token Reset token
   * @param newPassword New password
   * @returns Success message
   */
  async confirmPasswordReset(token: string, newPassword: string) {
    // Implementation will depend on your token validation logic
    this.logger.log(
      `Password reset confirmed with token: ${token} and new password: ${newPassword}`,
    );
    return { message: 'Password reset successful' };
  }

  /**
   * Verify access token
   * @param accessToken Access token to verify
   * @returns User information if token is valid
   */
  async verifyToken(accessToken: string) {
    try {
      // Verify token
      const payload = this.jwtService.verify(accessToken, {
        secret: this.configService.get('JWT_SECRET'),
      });

      // Get user
      const user = await this.usersService.findById(payload.sub);
      if (!user) {
        throw new UnauthorizedException('User not found2');
      }

      // Check if session exists
      const session = await this.redisService.getSession(payload.sessionId);
      if (!session) {
        throw new UnauthorizedException('Session expired');
      }

      // Return different response based on user role
      if (user.role === UserRole.ADMIN) {
        return {
          userId: user.id,
          userRole: user.role,
          status: AuditStatus.PASS, // Admin users are always PASS
        };
      } else {
        // For regular users, check device status
        const auditStatus = await this.getUserAuditStatusBySerialNumber(
          user.id,
          session.deviceSerialNumber,
        );

        return {
          userId: user.id,
          userRole: user.role,
          status: auditStatus,
        };
      }
    } catch (error) {
      this.logger.error(`Token verification failed: ${error.message}`);
      throw new UnauthorizedException('Invalid token');
    }
  }

  /**
   * Hash a password
   * @param password Plain text password
   * @returns Hashed password
   */
  private async hashPassword(password: string): Promise<string> {
    return argon2.hash(password);
  }

  /**
   * Verify a password
   * @param plainPassword Plain text password
   * @param hashedPassword Hashed password
   * @returns True if password is valid
   */
  private async verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {
    return argon2.verify(hashedPassword, plainPassword);
  }

  /**
   * Get user audit status based on device serial number
   * @param userId User ID
   * @param deviceSerialNumber Device serial number
   * @returns Audit status
   */
  private async getUserAuditStatusBySerialNumber(
    userId: string,
    deviceSerialNumber: string,
  ): Promise<AuditStatus> {
    try {
      // First ensure the device exists and belongs to the user
      const device = await this.devicesService.findBySerialNumber(deviceSerialNumber);
      if (!device || device.userId !== userId) {
        return AuditStatus.NOT_PASS;
      }

      // Get the audit status from DeviceAuditService
      try {
        if (!this.moduleRef) {
          this.logger.error('ModuleRef is not available');
          throw new InternalServerErrorException('ModuleRef not available');
        }

        // Use proper typing for DeviceAuditService
        let deviceAuditService: DeviceAuditService;
        try {
          deviceAuditService = this.moduleRef.get(DeviceAuditService, { strict: false });
        } catch (error) {
          throw new InternalServerErrorException('DeviceAuditService not available');
        }

        if (!deviceAuditService) {
          this.logger.error('DeviceAuditService not found in ModuleRef');
          throw new InternalServerErrorException('DeviceAuditService not available');
        }

        const auditStatusResult = await deviceAuditService.getAuditStatus(deviceSerialNumber);
        return auditStatusResult.status;
      } catch (error) {
        this.logger.error(`Error with DeviceAuditService: ${error.message}`);
        if (error instanceof InternalServerErrorException) {
          throw error;
        }
        throw new InternalServerErrorException('Failed to get audit status: ' + error.message);
      }
    } catch (error) {
      this.logger.error(`Error fetching audit status: ${error.message}`);

      // Rethrow InternalServerErrorException
      if (error instanceof InternalServerErrorException) {
        throw error;
      }

      return AuditStatus.NOT_PASS;
    }
  }
}
